<?php
/**
 * @license http://www.gnu.org/licenses/lgpl.html LGPL
 * @author Harlan Iverson <h dot iverson at gmail dot com>
 * @copyright Copyright (c) 2006, Harlan Iverson.
 * @version $Id: FacesContext.class.php 22 2006-08-04 05:02:56Z Harlan $
 */


class PhpFaces_VariableNotFoundException extends Exception {
	function __construct( $variable ) {
		parent::__construct( "Could not resolve '$variable'");
	}
}

class PhpFaces_FacesContext {
	
	const SCOPE_REQUEST = "request";
	const SCOPE_SESSION = "session";
	const REQUEST_OBJECT_SEPERATOR = "__";
	const REQUEST_PREFIX = "faces-";
	const ACTION_PREFIX = "action-";
	
	private static $currentInstance = null;
	
	private $navigationRules;
	private  $beanContext;
	/** @todo make accessors */
	public $localContext;
	

	/**
	 * Setup the context
	 */
	public function __construct() {
			$this->beanContext = array();
			$this->localContext = array();
			$this->navigationRules = array();
	}
	
	/**
	 * Get the current instance
	 * @deprecated 8 - Jul 30, 2006 Use getCurrentInstance instead
	 */
	public static function &currentInstance() {
		if( self::$currentInstance == null ) {
			self::setCurrentInstance( new PhpFaces_FacesContext() );	
		}
		return self::$currentInstance;	
	}
	
	/**
	 * Get the current FacesContext instance
	 */
	public static function &getCurrentInstance() {
		return self::currentInstance();
	}
	
	/**
	 * Set the current FacesContext instance
	 */
	public static function setCurrentInstance( &$instance ) {
		self::$currentInstance = $instance;	
	}

	
	/**
	 * Resolve an EL expression to local context...
	 * This will soon be succeeded by a VariableResolver class
	 * @param $el string the EL expression to look up
	 */
	public function &resolve( $el ) {
		if( preg_match( FaceletsTemplateResource_VARIABLE_PATTERN, $el ) ) {
			$elFragment = el_to_path( $el );
		} else {
			return $el;
		}

		$varRef = null;
		
		// first try the bean context
		try {
			$varRef =& $this->resolveFromContext( $elFragment, $this->beanContext );
			// then try the local context
		} catch( PhpFaces_VariableNotFoundException $e ) {
			$varRef =& $this->resolveFromContext( $elFragment, $this->localContext );
		}
		
		return $varRef;
	}
	
	
	/**
	 * Resolve a variable from a "context," which is currently just an associatave array. It will
	 * take an EL path and traverse objects and assoc arrays recusively until it finds
	 * the expression you were looking for. It will throw a PhpFaces_VariableNotFoundException if
	 * it does not find the desired path. 
	 * @param $elFragment EL expression relative to $context
	 * @param &$context the context to resolve the next level of $beanPath in
	 * @return a reference to the requested variable; a callable reference if it was a method.
	 */
	protected function &resolveFromContext( $elFragment, &$context ) {
		if( $context == null || (is_array( $context ) && count( $context ) == 0 ) ) {
			throw new PhpFaces_VariableNotFoundException( "Empty context trying to resolve $elFragment" );
		}
		
		$varParts = split( "\.", $elFragment );
		
		$varRef = $context;
		
		for( $i = 0; $i < count($varParts); $i++ ) {
			$partName = $varParts[$i];  
			if( is_array( $varRef ) && array_key_exists( $partName, $varRef ) ) {
				$varRef =& $varRef[$partName];
			} else if( is_object( $varRef ) && property_exists( $varRef, $partName ) ) {
				$varRef =& $varRef->$partName;
			} else if( is_object( $varRef ) && method_exists( $varRef, "get".$partName ) ) {
				$accessor = "get".$partName;
				$varRef =& $varRef->$accessor();
			} else if( is_object( $varRef ) && method_exists( $varRef, $partName ) ) {	
				$actionRef = array( &$varRef, $partName);
				$varRef =& $actionRef;;
			} else {
				throw new PhpFaces_VariableNotFoundException( $elFragment );
			}
		}
		
		return $varRef;;
	}

	
	/**
	 * Runs the first (and should be only) action found and 
	 * returns out outcome string.
	 * @param &$fromContext mixed the context from which to extract the action name. generally $_POST.
	 * @return string the outcome, as returned by the callable action
	 */
	public function runAction( &$fromContext ) {
		$actionRef =& $this->getAction( $fromContext );
		
		if( $actionRef != null ) {	
			//echo "run action: "; print_r($actionRef);
			
			$outcome = call_user_func( $actionRef );
			
		return $outcome;
		}

	}
	/**
	 * Lookup the current action that should be executed from a given context
	 * @param &$fromContext mixed the context from which to extract the action name. generally $_POST.
	 * @return mixed a callable reference to the action to execute
	 */
	public function &getAction( &$fromContext ) {

		$action = $this->getCurrentActionName( $fromContext );
		$action = str_replace( self::REQUEST_OBJECT_SEPERATOR, ".", $action );

		if( $action ) {
			$action = '#{'.$action.'}';
		}
		$actionRef =& $this->resolve( $action ); // ...

		return $actionRef ;
	}
	
	
	/**
	 * Works the same as getRequestName, but instread resolves the name of an action.
	 * @see getRequestName
	 */
	public function getActionName( $el ) {
		$var = el_to_path( $el );
		return self::ACTION_PREFIX.str_replace( ".", self::REQUEST_OBJECT_SEPERATOR, $var );
	}
	
	
	/**
	 * Look up the name (EL) of the current action
	 * @param &$fromContext mixed the context from which to extract the action name. generally $_POST.
	 * @todo handle _x/_y suffic of image buttons as submitted by IE
	 */
	public function getCurrentActionName( &$fromContext ) {
		foreach( $fromContext as $k => $v ) {
			if( substr( $k, 0, strlen(self::ACTION_PREFIX) ) == self::ACTION_PREFIX 
				&& substr( $k, -2 ) != "_x" /* */
				&& substr( $k, -2 ) != "_y") {
					
				$action = substr( $k, strlen(self::ACTION_PREFIX) );
				$action = str_replace( self::REQUEST_OBJECT_SEPERATOR, ".", $action );
				return $action; //
			}
		}
	}

	/**
	 * Returns a POST variable name to be used on forms for certain expressions.
	 * The way this is done is nothing like JSF, and should perhaps be rethought.
	 * The reason for deviation is that PhpFaces simply does not have a StateHolder,
	 * or unique clientIds for each component. 
	 * @var $el string the EL expression to turn into a variable name
	 */
	public function getRequestName( $el ) {
		$el = el_to_path( $el );
		return self::REQUEST_PREFIX.str_replace( ".", self::REQUEST_OBJECT_SEPERATOR, $el );
	}
	
	/**
	 * Get the value of a request variable. I don't think this is used anymore...
	 * @todo see if this is used anymore
	 * @param $el string the EL expression to look up
	 */
	public function getRequestValue( $el ) {
		list( $beanName, $rest ) = split( "\.", $el, 2 );
		$rest = str_replace(".", "->", $rest);;
		return '{$'.$beanName.'->'.$rest.'}';;
	}
	
	
	
	/**
	 * Register a bean into the bean context
	 */
	public function registerBean( $name, $objectClass, $scope ) {

		$object = null;

		if( $scope == self::SCOPE_SESSION ) {
			if( isset( $_SESSION[ self::SCOPE_SESSION ] )
				&& isset( $_SESSION[ self::SCOPE_SESSION ][$name] ) ) {
				$object =& $_SESSION[ self::SCOPE_SESSION ][$name];
			} else {
				$object = new $objectClass;
				$_SESSION[ self::SCOPE_SESSION ][$name] =& $object ;
			}	
		} else { // request scope
			$object = new $objectClass;
		}

		$this->beanContext[ $name ] =& $object;
	}
	
	/**
	 * Get an assoc aray of all registered beans
	 */
	public function &getRegisteredBeans() {
		return $this->beanContext;	
	}
	
	/**
	 * Register an array of outcome handlers
	 * @todo rename this / get rid of it
	 * @params &$navigationRules PhpFaces_NavigationRuleConfigEntry[] an array of navigation rule entries
	 */
	public function registerNavigationRules( &$navigationRules ) {
		for( $i = 0; $i < count( $navigationRules ); $i++ ) {
			$this->registerNavigationRule( $navigationRules[ $i ] );
		}
	}
	
	/**
	 * Register a single outcome handler
	 * @todo rename this / get rid of it
	 * @param PhpFaces_NavigationRuleConfigEntry a single rule
	 */
	public function registerNavigationRule( &$navigationRule ) {
		$this->navigationRules[] = $navigationRule;
	}
	
	/**
	 * Get an array of outcome handlers
	 * @todo rename this / get rid of it
	 * @return PhpFaces_NavigationRuleConfigEntry[] 
	 */
	public function getNavigationRules() {
		return $this->navigationRules;
	}


	/**
	 * The "Assign Values" phase of JSF... Takes request variables, as mapped by 
	 * {@see getRequestName} and {@see getActionName}, and assigns them to actual 
	 * beans
	 * @param &$fromContext mixed the context from which to extract these values. generally $_POST.
	 * @return boolean true if there were no validation errors, false otherwise
	 * @todo Implement FacesMessage stuff
	 */
	public function assignValues( &$fromContext ) {

		$isValid = false;

		/* while */ foreach( $fromContext as $k => $v ) {
			if( substr( $k, 0, strlen(PhpFaces_FacesContext::REQUEST_PREFIX) ) == self::REQUEST_PREFIX ) {

				$varName = substr( $k, strlen(self::REQUEST_PREFIX) );
				$varName = str_replace( self::REQUEST_OBJECT_SEPERATOR, ".", $varName );

				$requiredSuffix = "-required";
				$isRequired = false;

				if( substr( $varName, -1*strlen($requiredSuffix) ) == $requiredSuffix ) {
					$varName = substr( $varName, 0, -1*strlen( $requiredSuffix ) );
					$isRequired = true;
				}
				$isEmpty = (!$v || !trim($v));

				if( $isRequired && !$isEmpty ) {
					$isValid = true;
					continue;
				}
				if( $varName ) {
					$varName = '#{'.$varName.'}';
				}
				$varRef =& $this->resolve( $varName );

				$varRef = $v;
			}
		}

		$success = !$isValid;
		return $success;		
	}
}

/**
 * Extract an EL from its prefix/suffix #{expression}
 * @param $el string the EL, including brackets, to resolve
 * @return string the name, minus brackets
 */
function el_to_path( $el ) {
	if( preg_match( FaceletsTemplateResource_VARIABLE_PATTERN, $el ) ) {
		preg_match( FaceletsTemplateResource_VARIABLE_PATTERN, $el, $m );
		
		//echo "resolve $el as $m[2]\n";
		
		return $m[2];
	}
}
?>